var json = json || {};
var text = text || require('./text');

json.TextReader = class {
  static open(data) {
    const decoder = text.Decoder.open(data);
    let state = 'start';
    for (let i = 0; i < 0x1000; i++) {
      const c = decoder.decode();
      if (c === undefined || c === '\0') {
        if (i === 0) {
          return null;
        }
        break;
      }
      if (c <= ' ') {
        if (c !== ' ' && c !== '\n' && c !== '\r' && c !== '\t') {
          return null;
        }
        continue;
      }
      switch (state) {
        case 'start':
          if (c === '#') {
            state = 'comment';
            break;
          }
          if (c === '[') {
            state = 'list';
            break;
          }
          if (c === '{') {
            state = 'object';
            break;
          }
          if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
            state = '';
            break;
          }
          return null;
        case 'list':
          if (c === '"' || c === '-' || c === '+' || c === '{' || c === '[' || (c >= '0' && c <= '9')) {
            state = '';
            break;
          }
          return null;
        case 'object':
          if (c != '"') {
            return null;
          }
          state = '';
          continue;
      }
    }
    return new json.TextReader(data);
  }

  constructor(data) {
    this._data = data;
    this._escape = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
  }

  read() {
    const decoder = text.Decoder.open(this._data);
    const stack = [];
    this._decoder = decoder;
    this._position = 0;
    this._char = decoder.decode();
    this._whitespace();
    let obj = undefined;
    let first = true;
    for (;;) {
      if (Array.isArray(obj)) {
        this._whitespace();
        let c = this._char;
        if (c === ']') {
          this._next();
          this._whitespace();
          if (stack.length > 0) {
            obj = stack.pop();
            first = false;
            continue;
          }
          if (this._char !== undefined) {
            this._unexpected();
          }
          return obj;
        }
        if (!first) {
          if (this._char !== ',') {
            this._unexpected();
          }
          this._next();
          this._whitespace();
          c = this._char;
        }
        first = false;
        switch (c) {
          case '{': {
            this._next();
            stack.push(obj);
            const item = {};
            obj.push(item);
            obj = item;
            first = true;
            break;
          }
          case '[': {
            this._next();
            stack.push(obj);
            const item = [];
            obj.push(item);
            obj = item;
            first = true;
            break;
          }
          default: {
            obj.push(c === '"' ? this._string() : this._literal());
            break;
          }
        }
      } else if (obj instanceof Object) {
        this._whitespace();
        let c = this._char;
        if (c === '}') {
          this._next();
          this._whitespace();
          if (stack.length > 0) {
            obj = stack.pop();
            first = false;
            continue;
          }
          if (this._char !== undefined) {
            this._unexpected();
          }
          return obj;
        }
        if (!first) {
          if (this._char !== ',') {
            this._unexpected();
          }
          this._next();
          this._whitespace();
          c = this._char;
        }
        first = false;
        if (c === '"') {
          const key = this._string();
          switch (key) {
            case '__proto__':
            case 'constructor':
            case 'prototype':
              throw new json.Error("Invalid key '" + key + "'" + this._location());
          }
          this._whitespace();
          if (this._char !== ':') {
            this._unexpected();
          }
          this._next();
          this._whitespace();
          c = this._char;
          switch (c) {
            case '{': {
              this._next();
              stack.push(obj);
              const value = {};
              obj[key] = value;
              obj = value;
              first = true;
              break;
            }
            case '[': {
              this._next();
              stack.push(obj);
              const value = [];
              obj[key] = value;
              obj = value;
              first = true;
              break;
            }
            default: {
              obj[key] = c === '"' ? this._string() : this._literal();
              break;
            }
          }
          this._whitespace();
          continue;
        }
        this._unexpected();
      } else {
        const c = this._char;
        switch (c) {
          case '{': {
            this._next();
            this._whitespace();
            obj = {};
            first = true;
            break;
          }
          case '[': {
            this._next();
            this._whitespace();
            obj = [];
            first = true;
            break;
          }
          default: {
            const value = c === '"' ? this._string() : c >= '0' && c <= '9' ? this._number() : this._literal();
            this._whitespace();
            if (this._char !== undefined) {
              this._unexpected();
            }
            return value;
          }
        }
      }
    }
  }

  _next() {
    if (this._char === undefined) {
      this._unexpected();
    }
    this._position = this._decoder.position;
    this._char = this._decoder.decode();
  }

  _whitespace() {
    while (this._char === ' ' || this._char === '\n' || this._char === '\r' || this._char === '\t') {
      this._next();
    }
  }

  _literal() {
    const c = this._char;
    if (c >= '0' && c <= '9') {
      return this._number();
    }
    switch (c) {
      case 't':
        this._expect('true');
        return true;
      case 'f':
        this._expect('false');
        return false;
      case 'n':
        this._expect('null');
        return null;
      case 'N':
        this._expect('NaN');
        return NaN;
      case 'I':
        this._expect('Infinity');
        return Infinity;
      case '-':
        return this._number();
    }
    this._unexpected();
  }

  _number() {
    let value = '';
    if (this._char === '-') {
      value = '-';
      this._next();
    }
    if (this._char === 'I') {
      this._expect('Infinity');
      return -Infinity;
    }
    const c = this._char;
    if (c < '0' || c > '9') {
      this._unexpected();
    }
    value += c;
    this._next();
    if (c === '0') {
      const n = this._char;
      if (n >= '0' && n <= '9') {
        this._unexpected();
      }
    }
    while (this._char >= '0' && this._char <= '9') {
      value += this._char;
      this._next();
    }
    if (this._char === '.') {
      value += '.';
      this._next();
      const n = this._char;
      if (n < '0' || n > '9') {
        this._unexpected();
      }
      while (this._char >= '0' && this._char <= '9') {
        value += this._char;
        this._next();
      }
    }
    if (this._char === 'e' || this._char === 'E') {
      value += this._char;
      this._next();
      const s = this._char;
      if (s === '-' || s === '+') {
        value += this._char;
        this._next();
      }
      const c = this._char;
      if (c < '0' || c > '9') {
        this._unexpected();
      }
      value += this._char;
      this._next();
      while (this._char >= '0' && this._char <= '9') {
        value += this._char;
        this._next();
      }
    }
    return +value;
  }

  _string() {
    let value = '';
    this._next();
    while (this._char != '"') {
      if (this._char === '\\') {
        this._next();
        if (this._char === 'u') {
          this._next();
          let uffff = 0;
          for (let i = 0; i < 4; i++) {
            const hex = parseInt(this._char, 16);
            if (!isFinite(hex)) {
              this._unexpected();
            }
            this._next();
            uffff = uffff * 16 + hex;
          }
          value += String.fromCharCode(uffff);
        } else if (this._escape[this._char]) {
          value += this._escape[this._char];
          this._next();
        } else {
          this._unexpected();
        }
      } else if (this._char < ' ') {
        this._unexpected();
      } else {
        value += this._char;
        this._next();
      }
    }
    this._next();
    return value;
  }

  _expect(value) {
    for (let i = 0; i < value.length; i++) {
      if (value[i] !== this._char) {
        this._unexpected();
      }
      this._next();
    }
  }

  _unexpected() {
    let c = this._char;
    if (c === undefined) {
      throw new json.Error('Unexpected end of JSON input.');
    } else if (c === '"') {
      c = 'string';
    } else if ((c >= '0' && c <= '9') || c === '-') {
      c = 'number';
    } else {
      if (c < ' ' || c > '\x7F') {
        const name = Object.keys(this._escape).filter((key) => this._escape[key] === c);
        c = name.length === 1 ? '\\' + name : '\\u' + ('000' + c.charCodeAt(0).toString(16)).slice(-4);
      }
      c = "token '" + c + "'";
    }
    throw new json.Error('Unexpected ' + c + this._location());
  }

  _location() {
    let line = 1;
    let column = 1;
    this._decoder.position = 0;
    let c;
    do {
      if (this._decoder.position === this._position) {
        return ' at ' + line.toString() + ':' + column.toString() + '.';
      }
      c = this._decoder.decode();
      if (c === '\n') {
        line++;
        column = 1;
      } else {
        column++;
      }
    } while (c !== undefined);
    return ' at ' + line.toString() + ':' + column.toString() + '.';
  }
};

json.BinaryReader = class {
  static open(data) {
    const buffer = data instanceof Uint8Array ? data : data.peek();
    return new json.BinaryReader(buffer);
  }

  constructor(buffer) {
    this._buffer = buffer;
  }

  read() {
    const buffer = this._buffer;
    const length = buffer.length;
    const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
    const asciiDecoder = new TextDecoder('ascii');
    const utf8Decoder = new TextDecoder('utf-8');
    let position = 0;
    const skip = (offset) => {
      position += offset;
      if (position > length) {
        throw new json.Error(
          'Expected ' + (position + length) + ' more bytes. The file might be corrupted. Unexpected end of file.',
          true
        );
      }
    };
    const header = () => {
      const start = position;
      skip(4);
      const size = view.getInt32(start, 4);
      if (size < 5 || start + size > length || buffer[start + size - 1] != 0x00) {
        throw new json.Error('Invalid file size.', true);
      }
    };
    header();
    const stack = [];
    let obj = {};
    for (;;) {
      skip(1);
      const type = buffer[position - 1];
      if (type == 0x00) {
        if (stack.length === 0) {
          break;
        }
        obj = stack.pop();
        continue;
      }
      const start = position;
      position = buffer.indexOf(0x00, start) + 1;
      const key = asciiDecoder.decode(buffer.subarray(start, position - 1));
      let value = null;
      switch (type) {
        case 0x01: {
          // float64
          const start = position;
          skip(8);
          value = view.getFloat64(start, true);
          break;
        }
        case 0x02: {
          // string
          skip(4);
          const size = view.getInt32(position - 4, true);
          const start = position;
          skip(size);
          value = utf8Decoder.decode(buffer.subarray(start, position - 1));
          if (buffer[position - 1] != '0x00') {
            throw new json.Error('String missing terminal 0.', true);
          }
          break;
        }
        case 0x03: {
          // object
          header();
          value = {};
          break;
        }
        case 0x04: {
          // array
          header();
          value = [];
          break;
        }
        case 0x05: {
          // bytes
          const start = position;
          skip(5);
          const size = view.getInt32(start, true);
          const subtype = buffer[start + 4];
          if (subtype !== 0x00) {
            throw new json.Error("Unknown binary subtype '" + subtype + "'.", true);
          }
          skip(size);
          value = buffer.subarray(start + 5, position);
          break;
        }
        case 0x08: {
          // boolean
          skip(1);
          value = buffer[position - 1];
          if (value > 1) {
            throw new json.Error("Invalid boolean value '" + value + "'.", true);
          }
          value = value === 1 ? true : false;
          break;
        }
        case 0x0a:
          // value should be null;
          break;
        case 0x10: {
          const start = position;
          skip(4);
          value = view.getInt32(start, true);
          break;
        }
        case 0x11: {
          // uint64
          const start = position;
          skip(8);
          value = view.getUint64(start, true).toNumber();
          break;
        }
        case 0x12: {
          // int64
          const start = position;
          skip(8);
          value = view.getInt64(start, true).toNumber();
          break;
        }
        default:
          throw new json.Error("Unknown value type '" + type + "'.", true);
      }
      if (Array.isArray(obj)) {
        if (obj.length !== parseInt(key, 10)) {
          throw new json.Error("Invalid array index '" + key + "'.", true);
        }
        obj.push(value);
      } else if (obj != null) {
        switch (key) {
          case '__proto__':
          case 'constructor':
          case 'prototype':
            throw new json.Error("Invalid key '" + key + "' at " + position.toString() + "'.", true);
        }
        obj[key] = value;
      }
      if (type === 0x03 || type === 0x04) {
        stack.push(obj);
        obj = value;
      }
    }
    if (position !== length) {
      throw new json.Error("Unexpected data at '" + position.toString() + "'.", true);
    }
    return obj;
  }
};

json.Error = class extends Error {
  constructor(message, binary) {
    super(message);
    this.name = binary ? 'BSON Error' : 'JSON Error';
  }
};

if (typeof module !== 'undefined' && typeof module.exports === 'object') {
  module.exports.TextReader = json.TextReader;
  module.exports.BinaryReader = json.BinaryReader;
}
